﻿namespace RulesEngine
{
    #region EventArgs classes

    public class BeforeRuleGroupExecuteEventArgs<TCandidate> : EventArgs
    {
        public string Name { get; set; }
        public string Description { get; set; }
        public TCandidate Candidate { get; set; }
    }

    public class AfterRuleGroupExecuteEventArgs<TCandidate> : EventArgs
    {
        public string Name { get; set; }
        public string Description { get; set; }
        public TCandidate Candidate { get; set; }
        public bool IsMatch { get; set; }
    }

    #endregion EventArgs classes

    public class RulesEngine<TCandidate>
    {
        public string Name { get; set; }
        public RuleTypeDictionary RuleTypes { get; set; }
        protected internal RulesEngineTypeDictionary RuleGroupResultProcessorTypes { get; set; }
        public List<RuleSet<TCandidate>> RuleSets { get; set; }

        #region Events

        public EventHandler<BeforeRuleGroupExecuteEventArgs<TCandidate>> OnBeforeRuleGroupExecute;
        public EventHandler<AfterRuleGroupExecuteEventArgs<TCandidate>> OnAfterRuleGroupExecute;

        #endregion Events

        public RulesEngine()
        {
            RuleSets = new List<RuleSet<TCandidate>>();
        }

        public RuleSet<TCandidate> GetRuleSet(string name, DateTime compareDate)
        {
            var q = from ruleSet in RuleSets
                    where ruleSet.Name == name
                        && ruleSet.StartDate <= compareDate
                        && ruleSet.EndDate >= compareDate
                    select ruleSet;

            if (q.Any())
            {
                return q.First();
            }

            return null;
        }

        /// <summary>
        /// This will execute the RuleSet.  It will stop processing the groups and return a RuleSetResult
        /// when it finds the first RuleGroup that returns true.
        /// </summary>
        /// <param name="ruleSet">RuleSet</param>
        /// <param name="candidate">Candidate object against which the rules will be evaluated</param>
        /// <returns></returns>
        public virtual RuleSetResult ExecuteRuleSet(RuleSet<TCandidate> ruleSet, TCandidate candidate)
        {
            if (ruleSet == null)
            {
                throw new ApplicationException(string.Format("Cannot execute RuleSet because RuleSet is null"));
            }

            RuleSetResult result = new RuleSetResult();

            foreach (RuleGroup<TCandidate> ruleGroup in ruleSet.Groups)
            {
                //if (ruleGroup.Execute(candidate))
                //{
                //    result.Result = true;
                //    result.MatchedGroups.Add(ruleGroup.Name);

                //    var match = new RuleSetResultGroupMatch();
                //    match.Name = ruleGroup.Name;
                //    if (ruleGroup.ResultProcessor != null)
                //    {
                //        match.ResultObject = ruleGroup.ResultProcessor.GetResult(candidate);
                //    }
                //    result.MatchingGroups.Add(match);

                //    return result;
                //}

                if (ExecuteRuleGroup(ruleGroup, candidate, result))
                {
                    return result;
                }
            }

            return result;
        }

        /// <summary>
        /// This will execute the RuleSet.  It will stop processing the groups and return a RuleSetResult
        /// when it finds the first RuleGroup that returns true.
        /// </summary>
        /// <param name="name">Name of the RuleSet</param>
        /// <param name="compareDate">Date that is between the StartDate and EndDate of the RuleSet</param>
        /// <param name="candidate">Candidate object against which the rules will be evaluated</param>
        /// <returns></returns>
        public virtual RuleSetResult ExecuteRuleSet(string name, DateTime compareDate, TCandidate candidate)
        {
            RuleSet<TCandidate> ruleSet = GetRuleSet(name, compareDate);
            if (ruleSet == null)
            {
                throw new ApplicationException(string.Format("Cannot execute RuleSet because named RuleSet [{0}] does not exist in configuration for Date [{1:d}]", name, compareDate));
            }

            RuleSetResult result = new RuleSetResult();

            foreach (RuleGroup<TCandidate> ruleGroup in ruleSet.Groups)
            {
                //if (ruleGroup.Execute(candidate))
                //{
                //    result.Result = true;
                //    result.MatchedGroups.Add(ruleGroup.Name);

                //    var match = new RuleSetResultGroupMatch();
                //    match.Name = ruleGroup.Name;
                //    if (ruleGroup.ResultProcessor != null)
                //    {
                //        match.ResultObject = ruleGroup.ResultProcessor.GetResult(candidate);
                //    }
                //    result.MatchingGroups.Add(match);

                //    return result;
                //}

                if (ExecuteRuleGroup(ruleGroup, candidate, result))
                {
                    return result;
                }
            }

            return result;
        }

        /// <summary>
        /// This will execute the RuleSet.  It will process the rules for all Groups and return a RuleSetResult
        /// that contains a list of MatchedGroups.
        /// </summary>
        /// <param name="name">Name of the RuleSet</param>
        /// <param name="compareDate">Date that is between the StartDate and EndDate of the RuleSet</param>
        /// <param name="candidate">Candidate object against which the rules will be evaluated</param>
        /// <returns></returns>
        public virtual RuleSetResult ExecuteRuleSetAllGroups(string name, DateTime compareDate, TCandidate candidate)
        {
            RuleSet<TCandidate> ruleSet = GetRuleSet(name, compareDate);
            if (ruleSet == null)
            {
                throw new ApplicationException(string.Format("Cannot execute RuleSet because named RuleSet [{0}] does not exist in configuration for Date [{1:d}]", name, compareDate));
            }

            return ExecuteRuleSetAllGroups(ruleSet, candidate);
        }

        public virtual RuleSetResult ExecuteRuleSetAllGroups(RuleSet<TCandidate> ruleSet, TCandidate candidate)
        {
            if (ruleSet == null)
            {
                throw new ApplicationException(string.Format("Cannot execute RuleSet because RuleSet is null"));
            }

            RuleSetResult result = new RuleSetResult();

            foreach (RuleGroup<TCandidate> ruleGroup in ruleSet.Groups)
            {
                //if (ruleGroup.Execute(candidate))
                //{
                //    result.Result = true;
                //    result.MatchedGroups.Add(ruleGroup.Name);

                //    var match = new RuleSetResultGroupMatch();
                //    match.Name = ruleGroup.Name;
                //    if (ruleGroup.ResultProcessor != null)
                //    {
                //        match.ResultObject = ruleGroup.ResultProcessor.GetResult(candidate);
                //    }
                //    result.MatchingGroups.Add(match);

                //    //return result;
                //}

                ExecuteRuleGroup(ruleGroup, candidate, result);
            }

            return result;
        }

        private bool ExecuteRuleGroup(RuleGroup<TCandidate> ruleGroup, TCandidate candidate, RuleSetResult ruleSetResult)
        {
            if (OnBeforeRuleGroupExecute != null)
            {
                OnBeforeRuleGroupExecute(this, new BeforeRuleGroupExecuteEventArgs<TCandidate>() { Name = ruleGroup.Name, Description = ruleGroup.Description, Candidate = candidate });
            }

            bool result = ruleGroup.Execute(candidate);
            if (result == true)
            {
                ruleSetResult.Result = true;
                //ruleSetResult.MatchedGroups.Add(ruleGroup.Name);

                var match = new RuleSetResultGroupMatch();
                match.Name = ruleGroup.Name;
                if (ruleGroup.ResultProcessor != null)
                {
                    match.ResultObject = ruleGroup.ResultProcessor.GetResult(candidate);
                }
                ruleSetResult.MatchingGroups.Add(match);
            }

            if (OnAfterRuleGroupExecute != null)
            {
                OnAfterRuleGroupExecute(this, new AfterRuleGroupExecuteEventArgs<TCandidate>() { Name = ruleGroup.Name, Description = ruleGroup.Description, IsMatch = result, Candidate = candidate });
            }

            return result;
        }
    }
}